/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Save File to.
 *
 * The Initial Developer of the Original Code is
 * Atte Kemppilä.
 * Portions created by the Initial Developer are Copyright (C) 2009
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */


var EXPORTED_SYMBOLS =
[
    "savefileto"
];

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

Cu.import("resource://savefileto/CascadeMenu.jsm");
Cu.import("resource://savefileto/common.jsm");
Cu.import("resource://savefileto/constants.jsm");
Cu.import("resource://savefileto/DataSource.jsm");
Cu.import("resource://savefileto/Entry.jsm");
Cu.import("resource://savefileto/Preferences.jsm");
Cu.import("resource://savefileto/Transformer.jsm");

const Application = Cc["@mozilla.org/fuel/application;1"].getService(Ci.fuelIApplication);


var savefileto =
{
    get prefs()
    {
        var prefNames =
        [
            "page.enabled",
            "link.enabled",
            "image.enabled",
            "file.enabled",
            
            "page.accessKey",
            "link.accessKey",
            "image.accessKey",
            "file.accessKey",
            
            "alternatingMenuPosition",
            "mainMenuPosition",
            "maxMenuLength",
            "minMenuLength",
            "prompt",
            "recentSize",
            "responseTimeout",
            "startDownloadOnSelect",
            "subMenuPosition",
            "trimEmptyMenu"
        ];
        prefNames = new Set(prefNames);
        
        var prefs = new PreferencesProxy(EXT_PREF_BRANCH, prefNames);
        return lazyInit(this, "prefs", prefs);
    },
    
    get bundle()
    {
        var url = "chrome://savefileto/locale/savefileto.properties";
        return lazyInit(this, "bundle", Services.strings.createBundle(url));
    },
    
    get folderNameBundle()
    {
        var url = "chrome://savefileto/locale/folderNames.properties";
        return lazyInit(this, "folderNameBundle", Services.strings.createBundle(url));
    },
    
    get commandLabelBundle()
    {
        var url = "chrome://savefileto/locale/commandLabels.properties";
        return lazyInit(this, "commandLabelBundle", new StringBundle(url));
    },
    
    get folderFilter()
    {
        var glob = new PreferenceProxy(EXT_PREF_BRANCH + "filter");
        var fileNameFilter = new FileNameFilter(glob);
        var filter = function(file) { return fileNameFilter.filter(file.leafName); }
        return lazyInit(this, "folderFilter", filter);
    },
    
    get folderEntryTransformer()
    {
        var transformer = new FolderEntryTransformer(compare, this.folderFilter);
        return lazyInit(this, "folderEntryTransformer", transformer);
    },
    
    get recentFolderTransformer()
    {
        var transformer = new RecentFolderTransformer();
        return lazyInit(this, "recentFolderTransformer", transformer);
    },
    
    get namedFolderTransformer()
    {
        var transformer = new NamedFolderTransformer(this.folderNameBundle);
        return lazyInit(this, "namedFolderTransformer", transformer);
    },
    
    get namedFolderEntryTransformer()
    {
        var transformer = new ChainedTransformer(this.namedFolderTransformer, this.folderEntryTransformer);
        return lazyInit(this, "namedFolderEntryTransformer", transformer);
    },
    
    get commandLabelTransformer()
    {
        var transformer = new CommandLabelTransformer(this.commandLabelBundle);
        return lazyInit(this, "commandLabelTransformer", transformer);
    },
    
    mainDataSource:
    {
        get page()  { return lazyInit(this, "page",  savefileto._createMainDataSource("page")); },
        get link()  { return lazyInit(this, "link",  savefileto._createMainDataSource("link")); },
        get image() { return lazyInit(this, "image", savefileto._createMainDataSource("image")); },
        get file()  { return lazyInit(this, "file",  savefileto._createMainDataSource("file")); }
    },
    
    recentEntryDataSource:
    {
        get page()  { return lazyInit(this, "page",  savefileto._createRecentEntryDataSource("page")); },
        get link()  { return lazyInit(this, "link",  savefileto._createRecentEntryDataSource("link")); },
        get image() { return lazyInit(this, "image", savefileto._createRecentEntryDataSource("image")); },
        get file()  { return lazyInit(this, "file",  savefileto._createRecentEntryDataSource("file")); }
    },
    
    menuEntryDataSource:
    {
        get page()  { return lazyInit(this, "page",  savefileto._createMenuEntryDataSource("page")); },
        get link()  { return lazyInit(this, "link",  savefileto._createMenuEntryDataSource("link")); },
        get image() { return lazyInit(this, "image", savefileto._createMenuEntryDataSource("image")); },
        get file()  { return lazyInit(this, "file",  savefileto._createMenuEntryDataSource("file")); }
    },
    
    createCascadeMenu: function sft_createCascadeMenu(type, commandCallback)
    {
        var prefs = this.prefs;
        
        var config =
        {
            get alternatingMenuPosition() { return prefs.alternatingMenuPosition; },
            commandCallback: commandCallback,
            commandLabel: this.bundle.GetStringFromName("saveHere"),
            get dataSource() { return lazyInit(this, "dataSource", savefileto.menuEntryDataSource[type]); },
            mainMenuClass: "savefileto-mainMenu",
            get mainMenuPosition() { return prefs.mainMenuPosition; },
            get maxMenuLength() { return prefs.maxMenuLength; },
            get minMenuLength() { return prefs.minMenuLength; },
            subMenuClass: "savefileto-subMenu",
            get subMenuPosition() { return prefs.subMenuPosition; },
            get trimEmptyMenu() { return prefs.trimEmptyMenu; }
        }
        
        // don't allow main menu position configuration in save file dialog
        if (type == "file")
            delete config.mainMenuPosition;
        
        return new CascadeMenu(config);
    },
    
    getOpenPrefsEntry: function sft_getOpenPrefsEntry(type)
    {
        var command = "cmd_savefileto_openPrefs_" + type;
        var entry = {type: "item", command: command};
        return entry;
    },
    
    getOpenPrefsCommandEntry: function sft_getOpenPrefsCommandEntry(type)
    {
        var entry = this.getOpenPrefsEntry(type);
        var result = this.commandLabelTransformer.transform(entry);
        return result;
    },
    
    addToRecent: function sft_addToRecent(type, entry)
    {
        var dataSource = this.recentEntryDataSource[type];
        var maxSize = this.prefs.recentSize;
        var equals = function(a, b) ((a.pathLabel == b.pathLabel) && (a.path == b.path));
        
        this._addToRecent(entry, dataSource, maxSize, equals);
    },
    
    _addToRecent: function sft__addToRecent(entry, dataSource, maxSize, equals)
    {
        var entries = dataSource.entries;
        var length = entries.length;
        var maxIndex = Math.min(length, maxSize - 1) - 1;
        
        for (var index = 0; index <= maxIndex; index++)
            if (equals(entry, entries[index]))
                break;
        
        // if the entry is the first already, don't change ds
        // note that ds may be over maxSize at this point, but that's ok because view trims the ds
        if ((index == 0) && (index <= maxIndex))
            return;
        
        entries = entries.slice();
        
        if (index <= maxIndex)
            entries.splice(index, 1);
        
        entries.unshift(entry);
        
        if (entries.length > maxSize)
            entries = entries.slice(0, maxSize);
        
        dataSource.entries = entries;
    },
    
    _createMainDataSource: function sft__createMainDataSource(type)
    {
        var prefName = EXT_PREF_BRANCH + type + ".entries";
        var dataSource = new PreferenceDataSource(prefName);
        return dataSource;
    },
    
    _createRecentEntryDataSource: function sft__createRecentEntryDataSource(type)
    {
        var prefName = EXT_PREF_BRANCH + type + ".recent";
        
        // entry list transformers
        var getTransformer = new ArrayTransformer(this.folderEntryTransformer, true);
        var setTransformer = new ArrayTransformer(this.recentFolderTransformer, true);
        
        // limit recent list size
        var maxLength = {get value() { return savefileto.prefs.recentSize; }};
        getTransformer =
            new ChainedTransformer(
                getTransformer,
                new MaxLengthTransformer(maxLength));
        
        var dataSource =
            new TransformingDataSource(
                new PreferenceDataSource(prefName),
                getTransformer,
                setTransformer);
        
        return dataSource;
    },
    
    _createMenuEntryDataSource: function sft__createMenuEntryDataSource(type)
    {
        var mainDataSource = this.mainDataSource[type];
        var recentEntryDataSource = this.recentEntryDataSource[type];
        
        // transform data source entry list into cascade menu entry list
        var transformers =
        {
            folder: this.folderEntryTransformer,
            item: this.commandLabelTransformer,
            named: this.namedFolderEntryTransformer,
            recent: new DataSourceTransformer(recentEntryDataSource),
            separator: null
        }
        var transformer =
            new ArrayTransformer(
                new SwitchTransformer("type", transformers),
                true);
        
        var cachedDataSource =
            new CachingDataSource(
                new TransformingDataSource(mainDataSource, transformer));
        
        // reset cache when recent ds changes
        // (CachingDataSource handles internally changes in decorated ds (mainDs))
        recentEntryDataSource.addListener(function() { cachedDataSource.reset(); });
        
        // decorate cached data source with transformers whose result should not be cached
        var dataSource =
            new TransformingDataSource(
                cachedDataSource,
                new ChainedTransformer(
                    new EntryListTrimmer(),
                    new EmptyArrayTransformer([this.getOpenPrefsCommandEntry(type)])));
        
        return dataSource;
    },
    
    getMostRecentFolder: function(type)
    {
        var recentEntryDataSource =
            new TransformingDataSource(
                this.recentEntryDataSource[type],
                new EntryListTrimmer());
        
        var recent = recentEntryDataSource.entries;
        var folder = ((recent.length > 0) ? recent[0] : null);
        return folder;
    },
    
    getDownloadPrefs: function(docUri)
    {
        var prefs = Services.prefs.getBranch("browser.download.");
        
        var downloadPrefs = {};
        downloadPrefs.useDownloadDir = prefs.getBoolPref("useDownloadDir");
        downloadPrefs.folderList = prefs.getIntPref("folderList");
        
        try { downloadPrefs.dir = prefs.getComplexValue("dir", Ci.nsILocalFile); }
        catch (e) { /* ignore */ }
        
        try { downloadPrefs.lastDir = prefs.getComplexValue("lastDir", Ci.nsILocalFile); }
        catch (e) { /* ignore */ }
        
        // last download dir is remembered per site since ff7
        var lastDownloadDir = {uri: docUri, file: this._getLastDownloadDir(docUri)};
        
        return {downloadPrefs: downloadPrefs, lastDownloadDir: lastDownloadDir};
    },
    
    _getLastDownloadDir: function(docUri)
    {
        // get last download dir manually instead of using gDownloadLastDir.getFile()
        // because getFile() defaults to default last download dir if per-site dir doesn't exists
        
        try
        {
            let savePerSite = true;
            try { savePerSite = Services.prefs.getBoolPref("browser.download.lastDir.savePerSite"); }
            catch (e) { /* ignore */ }
            
            if (savePerSite)
            {
                let lastDir = Services.contentPrefs.getPref(docUri, "browser.download.lastDir");
                if (lastDir)
                {
                     var lastDirFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
                     lastDirFile.initWithPath(lastDir);
                     return lastDirFile;
                }
            }
        }
        catch (e) { /* ignore */ }
        
        return null;
    },
    
    setDownloadPrefs: function({downloadPrefs, lastDownloadDir})
    {
        var prefs = Services.prefs.getBranch("browser.download.");
        
        // in ff2, donwload manager window listens to pref changes, so the order here is important
        if (downloadPrefs.dir)
        {
            prefs.setComplexValue("dir", Ci.nsILocalFile, downloadPrefs.dir);
            prefs.setIntPref("folderList", downloadPrefs.folderList);
            prefs.setBoolPref("useDownloadDir", downloadPrefs.useDownloadDir);
        }
        else
        {
            prefs.setBoolPref("useDownloadDir", downloadPrefs.useDownloadDir);
            prefs.setIntPref("folderList", downloadPrefs.folderList);
            prefs.clearUserPref("dir");
        }
        
        if (downloadPrefs.lastDir)
            prefs.setComplexValue("lastDir", Ci.nsILocalFile, downloadPrefs.lastDir);
        else
            prefs.clearUserPref("lastDir");
        
        // reset per-site download dir
        // can't use gDownloadLastDir.setFile() because it doesn't support clearing the pref (and it might default to default pref)
        if (lastDownloadDir)
            if (lastDownloadDir.file)
                Services.contentPrefs.setPref(lastDownloadDir.uri, "browser.download.lastDir", lastDownloadDir.file.path);
            else
                Services.contentPrefs.removePref(lastDownloadDir.uri, "browser.download.lastDir");
    },
    
    openPrefWindow: function sft_openPrefWindow(pane)
    {
        var paneId = "prefpane_" + pane;
        
        var windowUrl = "chrome://savefileto/content/prefwindow/prefwindow.xul";
        var windowName = "savefileto_prefwindow";
        var win = Services.wm.getMostRecentWindow(windowName);
        
        if (win)
        {
            if (paneId)
            {
                var doc = win.document;
                var prefWindow = doc.documentElement;
                prefWindow.showPane(doc.getElementById(paneId));
            }
            
            win.focus();
        }
        else
        {
            var instantApply = Application.prefs.getValue("browser.preferences.instantApply", false);
            var features = "chrome,titlebar,toolbar,centerscreen" + (instantApply ? ",dialog=no" : ",modal");
            var args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
            var variant = Cc["@mozilla.org/variant;1"].createInstance(Ci.nsIWritableVariant);
            variant.setFromVariant(paneId);
            args.appendElement(variant, false);
            Services.ww.openWindow(null, windowUrl, windowName, features, args);
        }
    },
    
    cleanUp: function(force)
    {
        var prefs = Services.prefs.getBranch(EXT_PREF_BRANCH);
        if (! (force || prefs.getBoolPref("clearRecent")))
            return;
        
        for each (var type in TYPES)
            try
            {
                prefs.clearUserPref(type + ".recent");
            }
            catch (e) { /* ingore */ }
    }
}


/**
 * @param glob.value {string} filename glob
 */
function FileNameFilter(glob)
{
    this.glob = glob;
    
    var self = this;
    this.glob.addListener(function() { delete self._re; });
}

FileNameFilter.prototype =
{
    get re()
    {
        if (! this._re)
            try
            {
                // glob -> regexp
                var glob = this.glob.value.trim();
                if (glob)
                {
                    var flags = "g";  // global
                    var pattern = glob.replace(".", "\\.", flags).replace("*", ".*", flags).replace("?", ".", flags);
                    this._re = new RegExp("^(?:" + pattern + ")$");
                }
            }
            catch (e)
            {
                Cu.reportError(e);
            }
        
        return this._re;
    },
    
    filter: function FNF_filter(fileName)
    {
        var re = this.re;
        return (re ? re.test(fileName) : false);
    }
}


// nsIObserver
var observer =
{
    QueryInterface: XPCOMUtils.generateQI([
        Ci.nsIObserver,
        Ci.nsISupportsWeakReference]),
    
    observe: function(subject, topic, data)
    {
        if (topic == "quit-application")
            savefileto.cleanUp();
    }
}

Services.obs.addObserver(observer, "quit-application", true);
